-- DungeonInstanceM
-- Created by chengb Mar/13/2015
-- 地牢副本模块
-- 地牢副本只能在地牢中创建

module("DungeonInstanceM", package.seeall);

-- 所有的子模块
local rules = {};

-- 怪物的元素类型
local MONSTER_GRID_CLASS = 2;

-- 副本配置表
local instanceTable = {};

-- 副本建筑格子的位置
local buildingPos = 0;

-- 内部函数声明
local whenStartDungeon;
local whenLeaveDungeon;
local whenNextFloor;

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    instanceTable = {};

    -- 转换副本配置表
    local dungeon_instance_CSV = get_dungeon_instance_CSV();
    for i = 1, #dungeon_instance_CSV do
        local id    = dungeon_instance_CSV[i].id;
        local grade = dungeon_instance_CSV[i].grade;
        if not instanceTable[id] then
            instanceTable[id] = {};
        end
        instanceTable[id][grade] = dungeon_instance_CSV[i];
    end
    dungeon_instance_CSV = nil;
end

-- 初始化
function init()
    if not _init then
        loadCsv();

        -- 载入所有的规则处理子模块
        rules = LOAD_PATH("game/logic/module/dungeon_instance");
    end

    -- 关注地牢开始的事件
    EventMgr.removeAll("DungeonInstanceM");
    EventMgr.register("DungeonInstanceM", event.START_DUNGEON, function(layer)
        whenStartDungeon();
    end);

    -- 关注玩家离开地牢的事件
    EventMgr.register("DungeonInstanceM", event.LEAVE_DUNGEON, function()
        whenLeaveDungeon();
    end);

    -- 关注玩家进入下一层的回调
    EventMgr.register("DungeonInstanceM", event.NEXT_FLOOR, function()
        whenNextFloor();
    end);

    -- 关注玩家层数回退（大地之门等技能）的回调
    EventMgr.register("DungeonInstanceM", event.BACK_FLOOR, function()
        whenNextFloor();
    end);
end

-- 关注地牢开始的事件
function whenStartDungeon()

    -- 圣光印记，不能有UI的东西，注释掉 by panyl
    -- local arr = PropM.combine(ME.user, "light_imprint", 49);
    -- if UIDungeonMgr:getCurLevel() == nil and arr[3] > 0 then
    --     -- 触发特技发动事件
    --     ME.user.dbase:setTemp("speicial_skill_trigger", arr[1]);
    -- end

    -- 暂不做处理
end

-- 关注离开地牢的事件
function whenLeaveDungeon()
    ME.user.instance_stat = {};

    -- 移除副本备份信息
    removeInstanceBak();
end

-- 关注玩家进入下一层的事件
function whenNextFloor()
    -- 移除副本备份信息
    removeInstanceBak();
end

-- 移除副本备份信息
function removeInstanceBak()
    if ME.user.dbase:query("last_instance") ~= nil then
        ME.user.dbase:delete("last_instance");
    end

    if ME.user.dbase:query("instance_bak") ~= nil then
        ME.user.dbase:delete("instance_bak");
    end

    if ME.user.dbase:query("instance_context") ~= nil then
        -- 移除副本上下文数据
        ME.user.dbase:delete("instance_context");
    end
end

---------------------------
-- 获取副本信息
function getInstanceInfo(id, grade)
    -- 默认查询简单难度
    grade = grade or INSTANCE_GRADE_EASY;
    if not instanceTable[id] then
        return nil;
    end
    return instanceTable[id][grade];
end

------------------------------
-- 判断能否进入副本
-- @param id      副本id
-- @param grade   副本难度（如果没有传入，默认取简单）
function canStartInstance(id, grade)
    local grade = grade or INSTANCE_GRADE_EASY;
    local instanceInfo = getInstanceInfo(id, grade);
    if not instanceInfo then
        -- 获取不到副本信息
        return false;
    end

    if not DungeonM.isInDungeon() then
        -- 不在地牢中，不能创建地牢副本
        return false;
    end

    if isInInstance() then
        -- 已经在副本中了
        return false;
    end

    if ME.user:isDead() then
        trace("DungeonInstanceM", "玩家已死亡，无法进入副本。");
        return false;
    end

    return true;
end

------------------------------
-- 开始副本
-- @param pos    BuildingPos
-- @param id     副本id
-- @param grade  副本难度
function startInstance(pos, id, grade)
    pos = pos or 0;
    if not DungeonM.checkDungeonAction() then
        -- 经检测，不允许执行地牢行为
        return false;
    end

    assert(type(id) == "number");

    if not canStartInstance(id, grade) then
        -- 不满足进入条件
        return false;
    end

    DungeonLogM.addLog("[DungeonInstanceM]尝试开始地牢副本。");

    -- 暂停当前地牢
    DungeonM.pauseDungeon();

    grade = grade or getLastGrade(id);

    -- 如果没有传入副本难度
    if not grade then
        -- 获取随机种子
        local randSeed = DungeonM.getRandSeed("fetch_instance");

        -- 抽取副本难度
        grade = fetchGradeBySeed(id, randSeed);
    end

    -- 创建副本
    local instance;
    local instanceBak = ME.user.dbase:query("instance_bak");
    if instanceBak then
        -- 如果有备份的副本数据,判断下是否进入的副本是同一个
        local lastInstanceId = instanceBak.instance_id;
        if lastInstanceId == id then
            -- 是同一个则使用备份数据
            instance = table.deepcopy(instanceBak);
        else
            -- 不是同一个则创建新的数据
            removeInstanceBak();
            instance = createInstance(id, grade);
        end

        -- 移除备份数据
        ME.user.dbase:delete("instance_bak");
    else
        -- 创建副本
        instance = createInstance(id, grade);
    end

    if type(instance) ~= 'table' then
        -- 副本创建失败了
        return false;
    end

    -- 备份当前地牢数据
    if not bakDungeonData() then
        return false;
    end

    -- 位置
    instance["pos"] = pos;

    -- TODO: 给跪了
    local layer = DungeonM.currentLayer();
    local combats = instance.combats;
    local arr = combats[1];

    combats[1] = nil;
    combats[layer] = arr;

    local broken = false;
    if pos > 0 then
        local grid = DungeonM.getGridByPos(pos);
        broken = grid.broken;
    end

    -- 处理已失效的格子
    local context = ME.user.dbase:query("dungeon_context");
    dealDisableGrids(instance, context, broken);

    -- 更新地牢数据
    DungeonM.updateDungeon(instance, context);

    -- 累积进入副本的次数
    addEnterTimes(id);

    -- 记录一下副本难度，如果没有通关，则下次进来时就不用再抽取了
    ME.user.dbase:set("last_instance", {
        ["id"]    = id,
        ["grade"] = grade,
    });

    local msg = string.format("[DungeonInstanceM]成功开始地牢副本(%d/%d)", id, grade);
    DungeonLogM.addLog(msg);

    EventMgr.fire(event.ENTER_DUNGEON_INSTANCE, { ["id"] = id, ["grade"] = grade, });

    return true;
end

-- 处理失效的格子
function dealDisableGrids(instance, context, broken)
    local layer = instance.currLayer;
    local grids = instance.combats[layer];

    -- 如果已经破碎了
    if 1 == broken then
        for pos = 1, GRID_SIZE do
            repeat
                local info = {};
                if context then
                    info = context["context"][pos];
                end

                -- 失效的格子
                if info.state == GRID_STATE_DISABLE and not info.bonus then
                    grids[pos] = { [0] = 0, };
                elseif info.state ~= GRID_STATE_DISABLE then
                    -- 如果是怪物，干掉
                    local class = table.keys(grids[pos])[1];
                    if DungeonM.query(class, "type") == GRID_TYPE_MONSTER then
                        grids[pos] = { [0] = 0, };
                    elseif info["class"] then
                        grids[pos] = { [info["class"]] = info["element"] };
                    end
                end
            until true;
        end
        return;
    end

    if not context then
        -- 没有上下文数据，不处理
        return;
    end

    local pos = 1;
    while grids[pos] and context["context"][pos] do
        local info = context["context"][pos];

        if info.state == GRID_STATE_DISABLE and not info.bonus then
            -- 失效的给子，直接变为空格
            grids[pos] = { [0] = 0 };
            trace("DungeonInstanceM", "格子%d已失效，直接刷成空格。", pos);
        else
            grids[pos] = { [info["class"]] = info["element"] };
        end

        pos = pos + 1;
    end

    instance["combats"][layer] = grids;
end

------------------------------
-- 开始副本
-- @param id        副本id
-- @param randSeed  随机种子
function fetchGradeBySeed(id, randSeed)
    if not instanceTable[id] then
        -- 副本不存在
        return -1;
    end

    local instanceList = table.values(instanceTable[id]);

    -- 根据难度升序排序
    table.sort(instanceList, function(a, b) return a.grade < b.grade; end);

    -- 根据随机种子抽取
    local instance = fetchElemBySeed(instanceList, randSeed)

    return instance["grade"];
end

------------------------------
-- 创建副本
-- @param id     副本id
-- @param grade  副本难度
-- @return 如果创建成功，返回副本数据，否则，返回false
function createInstance(id, grade)
    local instanceInfo = getInstanceInfo(id, grade);
    if not instanceInfo then
        -- 获取不到副本信息
        return false;
    end

    if not DungeonM.isInDungeon() then
        -- 不在地牢中，不能创建地牢副本
        return false;
    end

    local dungeon = DungeonM.getDungeonContainer();
    local layer   = DungeonM.currentLayer();

    -- 构建地牢副本数据
    local dungeonInstance = {
        identify  = dungeon.identify,       -- 识别号
        dungeonId = dungeon.dungeonId,      -- 地牢编号
        dungeon_id = dungeon.dungeonId,     -- 为了兼容服务器
        currLayer = layer,                  -- 当前处于第几层
        layer     = layer,                  -- 为了和服务器兼容
        combats   = {},                     -- 各层地牢数据（副本只需要一层数据）
        rand_seed = dungeon.rand_seed,      -- 随机种子，直接沿用当前地牢的数据
        keyPos    = {},                     -- 各层钥匙位置
        instance_id = id,                   -- 副本id
        grade     = grade,                  -- 难度
    };

    -- 生成一层的格子数据
    local grids = fetchGrids(instanceInfo);

    dungeonInstance["combats"][1] = grids;

    return dungeonInstance;
end

-- 离开副本
function leaveInstance()
    -- 如果当前不在副本中，不处理
    if not isInInstance() then
        return false;
    end

    -- 如果领取了副本奖励，则副本建筑消失
    local instanceId = getCurrentInstanceId();
    local pos = getCurrentInstancePos();
    local isDisapper = isRemoveInstance();

    -- 如果副本建筑不消失，则执行一次副本暂离，将副本数据备份起来
    if not isDisapper then
        DungeonM.pauseDungeon();

        -- 转移上下文数据，以便下次进副本时恢复现场
        local dungeonContext = ME.user.dbase:query("dungeon_context");
        ME.user.dbase:set("instance_context", table.deepcopy(dungeonContext));

        -- 备份副本地牢数据
        local dungeon = DungeonM.getDungeonContainer();

        -- FUCK
        local layer = DungeonM.currentLayer();
        local curr = dungeon.combats[layer];
        dungeon.combats[layer] = nil;
        dungeon.combats[1] = curr;
        ME.user.dbase:set("instance_bak", table.deepcopy(dungeon));
    else
        -- 通关了，移除最近的副本信息
        ME.user.dbase:delete("last_instance");

        -- 移除进入次数，以便下次抽到副本时给予确认提示
        local timesInfo = ME.user.dbase:queryTemp("instance_times", {});
        timesInfo[instanceId] = 0;
        ME.user.dbase:setTemp("instance_times", timesInfo);

        removeInstanceBak();
    end

    -- 还原当前地牢数据
    if not restoreDungeonData(instanceId) then
        return false;
    end

    -- 恢复地牢战斗
    if not DungeonM.restoreDungeonCombat() then
        return false;
    end

    DungeonLogM.addLog("[DungeonInstanceM]离开地牢副本。");

    if isDisapper then
        -- 抛出副本建筑消失的事件
        EventMgr.fire(event.INSTANCE_DISAPPER, { ["id"] = instanceId, ["pos"] = pos,});
    end
    return true;
end

-- 判断副本是否需要消失
function isRemoveInstance()
    local instanceInfo = getCurInstanceInfo();
    if not instanceInfo then
        return true;
    end

    local dbase = instanceInfo["dbase"];
    if type(dbase) == 'table' and dbase['repeat_enter'] == 1 then
        -- 配置了可反复进入
        return false;
    end

    -- 检查是否是不消失的迷宫
    local isRemain = FormulaM.invoke("IS_REMAIN_INSTANCE");
    if isRemain then
        return false;
    end

    if isBonusTaken() then
        -- 已经领取了奖励
        return true;
    end

    if DungeonM.isAllMonsterDead() and
        not haveBonus() then
        -- 已经清怪，但是没有奖励可领取（中心格子可能是类似工坊之类的建筑）
        return true;
    end

    return false;
end

-- 获取上一次进入时抽取的难度
function getLastGrade(id)
    local lastInstance = ME.user.dbase:query("last_instance");
    if not lastInstance or lastInstance.id ~= id then
        return nil;
    end
    return lastInstance.grade;
end

-- 获取上一次抽取的副本id
function getLastInstanceId()
    return ME.user.dbase:query("last_instance", "id", false);
end

-- 是否在地牢副本中
function isInInstance()
    local dungeon = DungeonM.getDungeonContainer();
    if not dungeon then
        return false;
    end

    if not dungeon["instance_id"] then
        return false;
    end

    return true;
end

-- 获取当前所在的副本id
function getCurrentInstanceId()
    if not isInInstance() then
        return nil;
    end

    local dungeon = DungeonM.getDungeonContainer();
    return dungeon.instance_id;
end

-- 获取当前副本位置
function getCurrentInstancePos()
    if not isInInstance() then
        return;
    end

    local dungeon = DungeonM.getDungeonContainer();
    return dungeon.pos;
end

-- 能否拾取目标
function canPickUpTarget()
    if not isInInstance() then
        -- 不在副本中
        return false;
    end

    if not DungeonM.isAllMonsterDead() then
        -- 还有怪物没死
        return false;
    end

    return true;
end

-- 是否领取了副本奖励
function isBonusTaken()
    -- 1 ~ 5格子，如果是铁柜
    for pos = 1, 5 do
        local grid = DungeonM.getGridByPos(pos);
        if grid.type == GRID_TYPE_IRON_CHEST and sizeof(grid.bonus) > 0 then
            return false;
        end
    end

    local grid = getTargetGrid();
    if grid and grid.state == GRID_STATE_DISABLE then
        -- 目标格子已失效
        return true;
    end

    local dungeon = DungeonM.getDungeonContainer();
    if dungeon.bonusFlag == true then
        return true;
    end

    return false;
end

----------------------
-- 生成格子
-- @param instanceInfo 副本配置信息
function fetchGrids(instanceInfo)
    local dungeonId = instanceInfo["dungeon_id"];
    local elements = DungeonM.getElementsByDungeon(dungeonId);

    if not elements or #table.keys(elements) <= 0  then
        -- 获取不到地牢元素数据
        return {};
    end

    -- 获取怪物分布图
    local monsterGrids = instanceInfo["monster_grids"] or {};

    -- 至少要留两个位置存放目标和传送门
    assert(#monsterGrids < GRID_SIZE - 1);

    local ret = {};
    local arr;
    local randSeed;
    local element;

    -- 元素出现统计
    local elemStat = {};

    for index = 1, GRID_SIZE do
        -- 某层的格子元素数据，应该是{ { class1 : element1, }, ... {class30 : element30 },  }这样的格式
        if table.indexOf(monsterGrids, index) ~= -1 then
            -- 1.将怪物放到分布图上
            arr = elements[MONSTER_GRID_CLASS];

            assert(#table.keys(arr) > 0);

            -- 获取随机种子
            randSeed = DungeonM.getRandSeed("instance_fetch_monster");

            -- 抽取元素
            element = fetchElement(arr, randSeed, elemStat)

            -- 更新元素统计
            elemStat[element] = elemStat[element] or 0;
            elemStat[element] = elemStat[element] + 1;

            table.insert(ret, { [MONSTER_GRID_CLASS] = element } );
        else
            -- if table.indexOf(center_pos, index) <= 0 then
            --     table.insert(ret, {[DungeonM.getClassByType(GRID_TYPE_BLOCK)] = 0 });
            -- else
            --     -- 其他格子先刷层刷成空格
                table.insert(ret, {[0] = 0 });
            -- end
        end
    end

    -- 2.放置目标
    local targetGrid = instanceInfo["target_grid"];
    if type(targetGrid) == 'number' then
        -- 确保配置正确
        assert(targetGrid >= 1 and targetGrid <= GRID_SIZE);

        local targetClass = instanceInfo["target_class"];

        local prefix = "formula_";
        if type(targetClass) == 'string' and string.startWith(targetClass, "formula_") then
            -- 调用公式计算
            local formulaName = string.sub(targetClass, string.len(prefix) + 1);
            targetClass = FormulaM.invoke(formulaName);
        end

        arr = elements[targetClass];

        if type(arr) ~= 'table' or #arr <= 0 then
            error(string.format("地牢(%d)没有配置class为%s的元素", dungeonId, tostring(targetClass)));
        end

        -- 抽取目标元素
        randSeed = DungeonM.getRandSeed("instance_fetch_target");
        local temp = fetchElemBySeed(arr, randSeed);
        if not temp then
            error("抽取目标元素失败了。");
            return ret;
        end
        element = temp.id;

        ret[targetGrid] = { [targetClass] = element };

        -- 统计目标
        statInstanceGrid(targetClass, element);
    end

    -- 传送门（默认放在最后一个格子上）
    local doorGrid = GRID_SIZE;
    local doorClass = DungeonM.getClassByType(GRID_TYPE_DOOR);

    local dbase = instanceInfo["dbase"];
    local appointElement = {};
    if type(dbase) == "table" and type(dbase["appoint_element"]) == "table" then
        appointElement = dbase["appoint_element"];
    end

    -- 3.放置其他元素
    local layout = instanceInfo["extend_layout"];
    if type(layout) == "table" then
        for gridPos, gridClass in pairs(layout) do
            if type(appointElement[gridPos]) == 'number' then
                -- 指定了元素
                element = appointElement[gridPos];
            else
                -- 未指定元素，从列表中抽取一个
                arr = elements[gridClass];
                if type(arr) ~= 'table' or #arr <= 0 then
                    error(string.format("地牢(%d)没有配置class为%s的元素", dungeonId, tostring(gridClass)));
                end

                -- 抽取元素
                randSeed = DungeonM.getRandSeed("instance_fetch_extend_layout");
                element = fetchElemBySeed(arr, randSeed)["id"];
            end

            ret[gridPos] = { [gridClass] = element };

            -- 如果是门，需要记录其位置
            if gridClass == doorClass then
                doorGrid = gridPos;
            end
        end
    end

    -- 4.放置传送门
    ret[doorGrid] = { [doorClass] = 0 };

    return ret;
end

-- 抽取元素
function fetchElement(elemList, randSeed, filter)
    filter = filter or {};
    local selectedList = {};
    local waitingList  = {};

    for _, info in pairs(elemList) do
        while true do
            -- 如果有数量限制，并且数量已达上限，过滤之
            filter[info["id"]] = filter[info["id"]] or 0;
            if type(info["count_limit"]) == 'number' and
               info["count_limit"] <= filter[info["id"]] then
               break;
            end

            if info["ratio"] == NUMBER_INFINITY then
                -- 将必定出现的元素加入列表
                table.insert(selectedList, info);
            else
                -- 将概率出现的元素加入候选列表
                table.insert(waitingList, info);
            end

            break;
        end
    end

    local choice = {};
    if #selectedList > 0 then
        -- 从必现列表中抽取一个
        choice = fetchElemBySeed(selectedList, randSeed);
    elseif #waitingList > 0 then
        -- 从候选列表中抽取一个
        choice = fetchElemBySeed(waitingList, randSeed)
    end

    if #table.keys(choice) == 0 then
        -- 没有抽到
        return 0;
    end

    return choice["id"];
end

-- 格子过滤器
function filterInstanceGrids(arr)
    for _, row in pairs(arr) do
        local class = row["class"];
        local dbase = DungeonM.query(class, "dbase");

        if type(dbase["times_limit"]) == "number" and
            DungeonStatM.getStatCount(class) >= dbase["times_limit"] then
            row["ratio"] = 0;
        end
    end
end

-- 备份当前地牢数据
function bakDungeonData()
    local dungeon = DungeonM.getDungeonContainer();
    if not dungeon then
        return false;
    end

    -- 将主地牢数据备份起来
    local dungeonBak = table.deepcopy(dungeon);
    ME.user.dbase:set("dungeon_bak", dungeonBak);

    -- 将主地牢的上下文数据备份起来
    local dungeonContext = ME.user.dbase:query("dungeon_context");
    local dungeonContextBak = table.deepcopy(dungeonContext);
    ME.user.dbase:set("dungeon_context_bak", dungeonContextBak);

    -- 移除上下文数据
    ME.user.dbase:delete("dungeon_context");

    -- 如果存在副本上下文数据，则
    local instanceContext = ME.user.dbase:query("instance_context");
    if instanceContext then
         -- 将副本上下文数据搬到dungeon_context中
         -- 这样做的目的是：为了startDungeon时能够根据dungeon_context恢复副本现场
         ME.user.dbase:set("dungeon_context", table.deepcopy(instanceContext));

         -- 移除副本上下文数据
         ME.user.dbase:delete("instance_context");
    end

    return true;
end

-- 还原当前地牢数据
function restoreDungeonData(instanceId)
    local dungeonContextBak = ME.user.dbase:query("dungeon_context_bak");

    -- 如果已经通关副本，则将副本建筑刷成空格
    if isRemoveInstance() then
        local context = dungeonContextBak["context"];
        for i = 1, #context do
            if type(context[i]) == "table" then
                local class = context[i].class;
                local gridType = DungeonM.query(class, "type");
                if gridType == GRID_TYPE_INSTANCE then
                    -- 标记为已失效
                    context[i].state = GRID_STATE_DISABLE;
                elseif gridType == GRID_TYPE_STEAM_MACHINE then
                    if instanceId == SteamMachineM.query(context[i].machine_id, "instance_id") then
                        -- 标记为已失效
                        context[i].state = GRID_STATE_DISABLE;
                    end
                end
            end
        end
    end

    local dungeonBak = ME.user.dbase:query("dungeon_bak");
    if not dungeonBak then
        return false;
    end

    local dungeon = table.deepcopy(dungeonBak);
    DungeonM.setDungeon(dungeon);

    -- 还原主地牢上下文数据
    ME.user.dbase:set("dungeon_context", table.deepcopy(dungeonContextBak));

    -- 移除地牢备份数据
    ME.user.dbase:delete("dungeon_bak");
    ME.user.dbase:delete("dungeon_context_bak");

    return true;
end

-- 救猪
function rescuePig(pos)
    if TiedPigM.rescuePig(pos)  then
        local dungeon = DungeonM.getDungeonContainer();
        local instanceId = dungeon.instance_id;

        -- 标记玩家领取了奖励
        dungeon.bonusFlag = true;

        -- 通关了，移除最近的副本信息
        ME.user.dbase:delete("last_instance");

        -- 移除进入次数，以便下次抽到副本时给予确认提示
        local timesInfo = ME.user.dbase:queryTemp("instance_times", {});
        timesInfo[instanceId] = 0;
        ME.user.dbase:setTemp("instance_times", timesInfo);

        -- 抛出事件
        EventMgr.fire(event.TAKE_INSTANCE_BONUS, { ["pos"] = pos, });

        return true;
    end

    return false;
end

-- 判断是否有副本奖励可领取
function haveBonus()
    local grid = getTargetGrid();
    if not grid then
        return false;
    end

    local gridType = grid.type;

    -- TODO 如下类型的目标格子没有奖励领取
    local specialList = { GRID_TYPE_BLACKSMITH, GRID_TYPE_PIXY_CAGE, GRID_TYPE_PIXY, GRID_TYPE_SUMMON, };

    if table.indexOf(specialList, gridType) ~= -1 then
        return false;
    end

    return true;
end

---------------------
-- 领取副本奖励
-- @param pos   宝箱格子位置
function doBonus(pos)
    if not canPickUpTarget() then
        -- 如果不满足拾取条件
        return false;
    end

    local dungeon = DungeonM.getDungeonContainer();
    local instanceId = dungeon.instance_id;
    local grade      = dungeon.grade;
    local layer      = dungeon.currLayer;
    local instanceInfo = getInstanceInfo(instanceId, grade);
    local formula = instanceInfo["bonus_formula"];

    if dungeon.bonusFlag == true then
        trace("DungeonInstanceM", "已经领取过副本奖励，不可重复领取。");
        return false;
    end

    if type(formula) ~= 'string' then
        return false;
    end

    local grid = DungeonM.getGridByPos(pos);
    if grid.state == GRID_STATE_DISABLE then
        trace("DungeonInstanceM", "已经领取过副本奖励，不可重复领取。");
        return false;
    end

    -- 通知建筑事件完成
    EventMgr.fire(event.BUILDING_EVENT_DONE, {["pos"] = pos,});

    -- 调用公式计算奖励
    local args = {["grid_type"] = grid.type, ["class"] = grid.class, };
    local bonus = FormulaM.invoke(formula, ME.user, args, grade, layer);

    assert(type(bonus) == 'table');

    if #bonus > 0 then
        -- 数组格式

        for index, arr in pairs(bonus) do
            BonusM.doBonus(arr, "dungeon_instance");
        end

        if DungeonM.isCostRound(grid) then
            -- 抛出一个回合事件
            EventMgr.fire(event.COMBAT_ROUND, { ["pos"] = pos, ["bonus"] = bonus, });
        end
    elseif #table.keys(bonus) > 0 then
        -- mapping格式

        -- 执行建筑奖励
        BuildingBonusM.doBuildingBonus(bonus, "dungeon_instance");

        if DungeonM.isCostRound(grid) then
            -- 抛出一个回合事件
            EventMgr.fire(event.COMBAT_ROUND, { ["pos"] = pos, ["bonus"] = bonus["bonus"], });
        end
    else
        -- 其他格式视为没有奖励
    end

    -- 标记玩家领取了奖励
    dungeon.bonusFlag = true;

    grid:changeState(GRID_STATE_DISABLE);

    -- 抛出事件
    EventMgr.fire(event.TAKE_INSTANCE_BONUS, { ["pos"] = pos, ["bonus"] = bonus, });

    return true;
end

-- 判断进入副本时是否需要弹出提示框
function isConfirmToEnter(id)
    if getEnterTimes(id) > 0 then
        -- 已经进入过了，无需提示
        return false;
    end
    return true;
end

-- 判断离开副本时是否需要弹出提示框
function isConfirmToLeave()
    if not DungeonM.isAllMonsterDead() then
        -- 还有怪物没死
        return false;
    end

    if not haveBonus() then
        -- 没有副本奖励
        return false;
    end

    if isBonusTaken() then
        -- 已经领取副本奖励
        return false;
    end

    return true;
end

-- 累积进入副本次数
function addEnterTimes(id)
    local timesInfo = ME.user.dbase:queryTemp("instance_times", {});
    if not timesInfo[id] then
        timesInfo[id] = 0;
    end

    timesInfo[id] = timesInfo[id] + 1;
    ME.user.dbase:setTemp("instance_times", timesInfo);
end

-- 获取副本进入次数
function getEnterTimes(id)
    local timesInfo = ME.user.dbase:queryTemp("instance_times", {});
    return timesInfo[id] or 0;
end

-- 记录副本建筑格子的位置
function setBuildingPos(pos)
    buildingPos = pos;
end

-- 获取目标格子的位置
function getTargetGridPos()
    local dungeon = DungeonM.getDungeonContainer();
    local instanceId = dungeon.instance_id;
    local grade      = dungeon.grade;
    local instanceInfo = getInstanceInfo(instanceId, grade);
    if not instanceInfo then
        return nil;
    end

    return instanceInfo["target_grid"];
end

-- 获取目标格子
function getTargetGrid()
    local dungeon = DungeonM.getDungeonContainer();
    local instanceId = dungeon.instance_id;
    local grade      = dungeon.grade;
    local instanceInfo = getInstanceInfo(instanceId, grade);
    if not instanceInfo then
        return nil;
    end

    -- 获取目标格子位置
    local targetPos = instanceInfo["target_grid"];

    local grids = DungeonM.getCurrentDungeon();
    return grids[targetPos];
end

-- 根据随机种子抽取副本
function getInstanceIdByRandSeed(randSeed, filter, instanceType)
    local ids = table.keys(instanceTable);
    table.sort(ids);

    -- 剔除掉需要过滤的id
    for _, i in pairs(filter) do
        table.removeItem(ids, i);
    end

    if #ids == 0 then
        -- 一个不剩了，返回默认值
        return 1;
    end

    -- 过滤类型不符的副本
    local newIds = {};
    for _, id in pairs(ids) do
        local instanceInfo = getInstanceInfo(id);
        if instanceInfo and instanceInfo["type"] == instanceType then
            table.insert(newIds, id);
        end
    end

    if #newIds == 0 then
        -- 一个不剩了，返回默认值
        return 1;
    end

    local instanceId = newIds[randSeed % #newIds + 1];

    trace("DungeonInstanceM", string.format("抽取副本(%d)", instanceId));

    return instanceId;
end

-- 获取正在进行的副本信息
function getCurInstanceInfo()
    if not isInInstance() then
        return nil;
    end

    local dungeon = DungeonM.getDungeonContainer();
    local instanceId = dungeon.instance_id;
    local grade      = dungeon.grade;
    return getInstanceInfo(instanceId, grade);
end

-- 获取目标格子
function getTargetGrid()
    local instanceInfo = getCurInstanceInfo();
    if not instanceInfo then
        -- 获取不到副本信息
        return nil;
    end

    local targetPos = instanceInfo["target_grid"];
    return DungeonM.getGridByPos(targetPos);
end

-- 统计副本格子
function statInstanceGrid(class, element)
    local stat = ME.user.instance_stat or {};
    local count = stat[class] or 0;
    stat[class] = count + 1;

    ME.user.instance_stat = stat;

    -- 迷宫攻击
    DungeonStatM.doStat(class, 1, element);
end

-- 次数
function getInstanceStat(class)
    local stat = ME.user.instance_stat or {};

    return stat[class] or 0;
end

-- 抽取副本id
-- @param args  dungeon_element表中的args字段
function fetchInstanceId(args)
    local instanceId = DungeonInstanceM.getLastInstanceId();
    if instanceId then
        -- 已经抽过了，直接使用
        return instanceId;
    elseif args.instance_id ~= nil then
        -- 配置表中指定了副本id
        return args.instance_id;
    elseif type(args.rule) == 'string' then
        -- 配置表中指定了子规则，调用子模块抽取
        local f = rules[args.rule];
        if not f then
            assert(false, string.format("子模块%s不存在。", args.rule));
        end

        return f.fetchInstanceId();
    else
        -- 随机抽取一个id使用
        local filterArr = {};

        -- 如果没有传入副本类型，默认为怪物巢穴
        local instanceType = args.instance_type or INSTANCE_TYPE_CAVE;
        local seed = DungeonM.getRandSeed("fetch_instance_id");
        return DungeonInstanceM.getInstanceIdByRandSeed(seed, filterArr, instanceType);
    end
end

-- 分本格子统计
function updateInstanceStat(buff)
    if not buff then
        return;
    end

    local buffer = Buffer.create(buff);
    local offset = 1;
    local size = Buffer.size(buffer);
    local stat = {};
    while (offset <= size) do
        local count = Buffer.getu8(buffer, offset);
        if (count > 0) then
            stat[offset - 1] = count;
        end

        -- 继续下一个字节
        offset = offset + 1;
    end

    ME.user.instance_stat = stat;
end

-- 打包副本统计数据（verify to gs）
function packInstanceStat(filter)
    local stat = ME.user.instance_stat or {};
    local ret = {__mapping = 1, };
    for class, count in pairs(stat) do
        if count > (filter[class] or 0) then
            ret[class] = count;
        end
    end

    return ret;
end
